package top.bulk.sql.interceptor;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.ObjectUtils;
import top.bulk.sql.util.BulkSqlUtil;

import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;

/**
 * 基于 mysql 拦截器实现完成的sql日志打印
 * 注意 ： Executor.update 方法不仅用于更新（UPDATE）和删除（DELETE）操作，还用于插入（INSERT）操作，所以这里只处理 update 即可
 *
 * @author 散装java
 * @date 2024-07-31
 */
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query"
                        , args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query"
                        , args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
                        , CacheKey.class, BoundSql.class}),
                @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        }
)
@Slf4j
public class BulkMybatisSqlPrintInterceptor implements Interceptor {
    // private static final String SQL_OUT_FORMAT = "\n==>    方法: {} \n==>    SQL: {}  \n==>    行数: {}  \n==>    耗时: {}ms";
    private static final String SQL_OUT_FORMAT = "\n====== 方法: {} ====== \n==>    SQL: {}  \n==>    行数: {}  \n====== 耗时: {}ms =====================";


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object proceed = null;
        try {
            long start = System.currentTimeMillis();
            proceed = invocation.proceed();
            long costTime = System.currentTimeMillis() - start;
            SqlDetail sqlDetail = getSqlDetail(invocation);
            log.info(SQL_OUT_FORMAT, sqlDetail.methodName, sqlDetail.sql, BulkSqlUtil.getReturnRows(proceed), costTime);
        } catch (Exception exception) {
            log.error("BulkMybatisSqlPrintInterceptor 异常:{}", exception.getMessage(), exception);
        }
        return proceed;
    }

    /**
     * 获取SQL语句和参数
     */
    private static SqlDetail getSqlDetail(Invocation invocation) {
        SqlDetail sqlDetail = new SqlDetail();
        // 获取statement
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        assert statement != null;

        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }

        // 获取执行方法名
        String methodLocate = statement.getId();
        sqlDetail.setMethodName(BulkSqlUtil.getMethodName(methodLocate, '.'));

        // Configuration 对象保存了 MyBatis 框架运行时所有的配置信息
        Configuration configuration = statement.getConfiguration();
        // BoundSql 对象存储了一条具体的 SQL 语句及其相关参数信息。
        BoundSql boundSql = statement.getBoundSql(parameter);

        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 获取参数映射
        List<ParameterMapping> params = boundSql.getParameterMappings();
        // 获取到执行的SQL
        String sql = boundSql.getSql();
        // SQL中多个空格使用一个空格代替
        sql = sql.replaceAll("[\\s]+", " ");
        if (!ObjectUtils.isEmpty(params) && !ObjectUtils.isEmpty(parameterObject)) {
            // TypeHandlerRegistry 是 MyBatis 用来管理 TypeHandler 的注册器。TypeHandler 用于在 Java 类型和 JDBC 类型之间进行转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果参数对象的类型有对应的 TypeHandler，则使用 TypeHandler 进行处理
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(BulkSqlUtil.getParameterValue(parameterObject)));
            } else {
                // 否则，逐个处理参数映射
                for (ParameterMapping param : params) {
                    // 获取参数的属性名
                    String propertyName = param.getProperty();
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    // 检查对象中是否存在该属性的 getter 方法，如果存在就取出来进行替换
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(BulkSqlUtil.getParameterValue(obj)));
                        continue;
                    }

                    // 检查 BoundSql 对象中是否存在附加参数。附加参数可能是在动态 SQL 处理中生成的，有的话就进行替换
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(BulkSqlUtil.getParameterValue(obj)));
                        continue;
                    }

                    // 如果都没有，说明SQL匹配不上，带上“参数缺失”方便找问题
                    sql = sql.replaceFirst("\\?", "参数缺失");
                }
            }
        }
        sqlDetail.setSql(sql);
        return sqlDetail;
    }


    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    public static class SqlDetail {
        private String sql;
        private String methodName;
    }

}
